ES裡 aggregate 的狀態由 events的紀錄所決定,所以要能夠從頭建立event,因此自帶audit log功能,若要進從不同時序的追蹤,使用snapshot可以減少逐項apply event的成本,而有時要還原到某時點在snapshot之前,使用rollback提供反向回滾能比較有效率處理,甚或因為事件的不可修改性,若真的遇到要修改歷歷誤植資料,也能成為處理手法的工具之一。
試跑昨日測試,先準備共用commands
// 圖書建檔
fn create_book_command() -> BookCommand {
BookCommand::CreateBook {
id: "test-book-id".to_string(),
title: "test-book-title".to_string(),
isbn10: "1234567890".to_string(),
description: "test-book-description".to_string(),
}
}
// 圖書入庫
fn ingest_book_command() -> BookCommand {
BookCommand::IngestBook {
id: "test-book-id".to_string(),
copies: 1,
}
}
// 圖書借閱
fn lending_book_command() -> BookCommand {
BookCommand::LendBook(
LendingRecord {
reader_id: "test-reader-id".to_string(),
lent_date: Utc::now(),
due_date: Utc::now().add(chrono::Duration::days(7)),
})
}
// 圖書歸還
fn return_book_command() -> BookCommand {
BookCommand::ReturnBook (
LentRecord {
reader_id: "test-reader-id".to_string(),
lent_date: Utc::now(),
due_date: Utc::now().add(chrono::Duration::days(7)),
returned_date: Some(Utc::now()),
}
)
}
書籍資料建檔
#[tokio::test]
async fn test_create_book() {
let command = create_book_command();
let mut aggregate = Book::default();
let events = aggregate.handle(command).await.unwrap();
for event in events {
aggregate.apply(event);
}
assert_eq!(aggregate.id, "test-book-id".to_string());
assert_eq!(aggregate.title, "test-book-title".to_string());
assert_eq!(aggregate.isbn10, "1234567890".to_string());
assert_eq!(aggregate.description, "test-book-description".to_string());
}
書籍入庫
#[tokio::test]
async fn test_ingest_book() {
let command = create_book_command();
let mut aggregate = Book::default();
let events = aggregate.handle(command).await.unwrap();
let event = &events[0];
aggregate.apply(event.clone());
let command = ingest_book_command();
let events = aggregate.handle(command).await.unwrap();
let event = &events[0];
aggregate.apply(event.clone());
assert_eq!(aggregate.copies, 1); // 庫存為1
aggregate.rollback(event.clone()); // 回滾事件
assert_eq!(aggregate.copies, 0); // 庫存為0
}
書籍借閱,happy path
#[tokio::test]
async fn test_lending_book() {
let command = create_book_command();
let mut aggregate = Book::default();
let events = aggregate.handle(command).await.unwrap();
let event = &events[0];
aggregate.apply(event.clone());
let command = ingest_book_command();
let events = aggregate.handle(command).await.unwrap();
let event = &events[0];
aggregate.apply(event.clone());
let command = lending_book_command();
let events = aggregate.handle(command).await.unwrap();
let event = &events[0];
aggregate.apply(event.clone());
assert_eq!(aggregate.lending_records.len(), 1); // 借閱紀錄 1 筆
assert_eq!(aggregate.lending_records[0].reader_id, "test-reader-id".to_string());
assert_eq!(aggregate.available_copies(), 0); // 庫存數量 0 本
aggregate.rollback(event.clone()); // 資料回滾
assert_eq!(aggregate.lending_records.len(), 0); // 借閱紀錄 0 筆
assert_eq!(aggregate.available_copies(), 1); // 庫存數量 1 本
}
書籍借閱,驗證檢核邏輯
#[tokio::test]
async fn test_lending_book_validations() {
// 準備資料入庫2本書,已借出一本
let mut aggregate = Book::default();
aggregate.apply(BookEvent::BookIngested {
id: "test-book-id".to_string(),
copies: 2,
});
let date = Utc::now();
aggregate.apply(BookEvent::BookLent(
LendingRecord {
reader_id: "test-reader-id".to_string(),
lent_date: date.clone(),
due_date: Utc::now().add(chrono::Duration::days(7)),
}));
// 同一讀書再行借閱
let err = aggregate.handle(
BookCommand::LendBook(LendingRecord {
reader_id: "test-reader-id".to_string(),
lent_date: date.clone(),
due_date: Utc::now().add(chrono::Duration::days(7)),
})).await.unwrap_err();
assert_eq!(err, book_lib::domain::book::BookError("讀者已借出同一本書".to_string()));
// 安排把另一本書也借走,剩餘庫存為0
aggregate.apply(BookEvent::BookLent(
LendingRecord {
reader_id: "test-reader-id-2".to_string(),
lent_date: date.clone(),
due_date: Utc::now().add(chrono::Duration::days(7)),
}));
assert_eq!(aggregate.available_copies(), 0);
// 安排另一讀書借閱
let err = aggregate.handle(
BookCommand::LendBook(LendingRecord {
reader_id: "test-reader-id".to_string(),
lent_date: date.clone(),
due_date: Utc::now().add(chrono::Duration::days(7)),
})).await.unwrap_err();
assert_eq!(err, book_lib::domain::book::BookError("書籍已無庫存".to_string()));
}
還書
#[tokio::test]
async fn test_return_book() {
// 準備已借出書籍
let mut aggregate = Book::default();
aggregate.apply(BookEvent::BookIngested {
id: "test-book-id".to_string(),
copies: 1,
});
aggregate.apply(BookEvent::BookLent(
LendingRecord {
reader_id: "test-reader-id".to_string(),
lent_date: Utc::now(),
due_date: Utc::now().add(chrono::Duration::days(7)),
}));
assert_eq!(aggregate.available_copies(), 0);
// 執行還書操作
let command = return_book_command();
let events = aggregate.handle(command).await.unwrap();
let event = &events[0];
aggregate.apply(event.clone());
assert_eq!(aggregate.lending_records.len(), 0);
assert_eq!(aggregate.lent_history.len(), 1);
assert_eq!(aggregate.available_copies(), 1);
// 回滾還書事件
aggregate.rollback(event.clone());
assert_eq!(aggregate.lending_records.len(), 1);
assert_eq!(aggregate.lent_history.len(), 0);
assert_eq!(aggregate.available_copies(), 0);
}